#include "httpserver.h"
#include "../tunnelconstructor.h"
#include "../memorymanagment.h"
#include "crow/mustache.h"
#include "../htmldata.h"
#include "../tunnelconstructor.h"
#include "../versionnumber.h"

HttpServer::HttpServer(const std::string &address, uint16_t port) : address_(address), port_(port)
{
    app_.loglevel(crow::LogLevel::Critical);

    setup();
}

void HttpServer::run()
{
    app_.bindaddr(address_).port(port_).multithreaded().run();
}

void HttpServer::setup()
{
    CROW_CATCHALL_ROUTE(app_)
    ([](crow::response& res) {
        if (res.code == 404)
        {
            res.body = "The URL does not seem to be correct.";
        }
        else if (res.code == 405)
        {
            res.body = "The HTTP method does not seem to be correct.";
        }
        res.end();
    });

    CROW_ROUTE(app_, "/generate/html")([&](const crow::request& req, crow::response& res){
        res.set_header("Content-Type", "text/html");

        const auto language = lang(req);

        auto constructor = initTunnelConstructorViaUrlQuery(req, language);
        if (constructor.second == false)
        {
            res.code = 400;
            res.write( errorPage(reinterpret_cast<const char*>(constructor.first.errorString().c_str()), language, query(req)));
            res.end();
            return;
        }

        const auto text = configPage( reinterpret_cast<const char*>(constructor.first.generate().c_str()) , language, constructor.first.isCommentsEnabled(), query(req));
        res.write(text);
        res.end();
    });

    CROW_ROUTE(app_, "/favicon.png")([&](const crow::request& req, crow::response& res){
        res.set_header("Content-Type", "image/png");
        res.add_header("Content-Length", std::to_string(sizeof(faviconB64)));
        res.write(std::string(reinterpret_cast<const char*>(faviconB64), sizeof(faviconB64)));
        res.end();
    });

    CROW_ROUTE(app_, "/")([&](const crow::request& req, crow::response& res){
        res.set_header("Content-Type", "text/html");

        const auto language = lang(req);

        const auto text = mainPage(language);
        res.write(text);
        res.end();
    });
}

Notepad::Lang HttpServer::lang(const crow::request &req)
{
    const auto lang = req.url_params.get("lang");
    if (lang)
    {
        auto& ctx = app_.get_context<crow::CookieParser>(req);
        ctx.set_cookie("lang", lang).path("/").max_age(60*60*24).httponly();
        return Notepad::stringToLang(lang);
    }

    auto& ctxCookie = app_.get_context<crow::CookieParser>(req);
    const std::string langFromCookie = ctxCookie.get_cookie("lang");
    if (langFromCookie.empty())
    {
        auto& ctxFromHeader = app_.get_context<LanguageHandler>(req);
        return ctxFromHeader.lang;
    }

    return langFromCookie == "en" ? Notepad::Lang::en : Notepad::Lang::ru;
}

std::pair<TunnelConstructor, bool> HttpServer::initTunnelConstructorViaUrlQuery(const crow::request &req, Notepad::Lang lang)
{
    TunnelConstructor constructor;
    constructor.setLang(lang);

    bool comments = false;
    int8_t inbound_length = 3;
    int8_t outbound_length = 3;
    int8_t inbound_quantity = 5;
    int8_t outbound_quantity = 5;
    int8_t inbound_lengthVariance = 0;
    int8_t outbound_lengthVariance = 0;
    int8_t keepalive = 0;

    for (const auto& key: req.url_params.keys())
    {
        if (key == "name" and not constructor.setName(req.url_params.get("name")))
        {
            return { constructor, false };
        }
        else if (key == "type" and not constructor.setTunnelType( TunnelConstructor::stringToTunnelType(req.url_params.get("type")) ))
        {
            return { constructor, false };
        }
        else if (key == "inbound.length")
        {
            try { inbound_length = std::stoi(req.url_params.get("inbound.length")); } catch (...) { }
        }
        else if (key == "outbound.length")
        {
            try { outbound_length = std::stoi(req.url_params.get("outbound.length")); } catch (...) { }
        }
        else if (key == "inbound.quntity")
        {
            try { inbound_quantity = std::stoi(req.url_params.get("inbound.quntity")); } catch (...) { }
        }
        else if (key == "outbound.quntity")
        {
            try { outbound_quantity = std::stoi(req.url_params.get("outbound.quntity")); } catch (...) { }
        }
        else if (key == "inbound.lengthVariance")
        {
            try { inbound_lengthVariance = std::stoi(req.url_params.get("inbound.lengthVariance")); } catch (...) { }
        }
        else if (key == "outbound.lengthVariance")
        {
            try { outbound_lengthVariance = std::stoi(req.url_params.get("outbound.lengthVariance")); } catch (...) { }
        }
        else if (key == "blinded" and not constructor.setBlindedLeaseSet(std::string(req.url_params.get("blinded")) == "true"))
        {
            return { constructor, false };
        }
        else if (key == "transient" and not constructor.setTransient(std::string(req.url_params.get("transient")) == "true"))
        {
            return { constructor, false };
        }
        else if (key == "comments")
        {
            comments = std::string(req.url_params.get("comments")) == "true";
        }
        else if (key == "keepalive")
        {
            try { keepalive = std::stoi(req.url_params.get("keepalive")); } catch (...) { }
        }
    }

    if (not constructor.setComments(comments))
    {
        return { constructor, false };
    }

    if (not constructor.setLength(inbound_length, outbound_length))
    {
        return { constructor, false };
    }

    if (not constructor.setQuantity(inbound_quantity, outbound_quantity))
    {
        return { constructor, false };
    }

    if (not constructor.setLengthVariance(inbound_lengthVariance, outbound_lengthVariance))
    {
        return { constructor, false };
    }

    if (not constructor.setKeepAlive(keepalive))
    {
        return { constructor, false };
    }

    return { constructor, true };
}

std::string HttpServer::query(const crow::request &req)
{
    std::string q;
    for (const auto& key: req.url_params.keys())
    {
        if (key == "lang") continue;

        if (q.empty())
        {
            q = "?";
        }
        else
        {
            q += "&";
        }

        q += key + "=" + req.url_params.get(key);
    }
    return q;
}

std::string HttpServer::configPage(const std::string& generated, Notepad::Lang lang, bool comments, const std::string& query)
{
    crow::mustache::context ctx;
    ctx["lang_code"] = Notepad::langToCode(lang);
    ctx["tagline"] = reinterpret_cast<const char*>(Notepad::WebUi::tagline(lang));
    ctx["payload"] = generated;
    ctx["go_back"] = reinterpret_cast<const char*>(Notepad::WebUi::configGoBack(lang));
    ctx["url"] = query;
    ctx["version"] = VERSION;

    static const auto templ = crow::mustache::compile(HTML_CONFIG_PAGE);
    std::string pageWithoutCommentsHighlight = templ.render_string(ctx);

    if (! comments) return pageWithoutCommentsHighlight;

    std::istringstream stream(pageWithoutCommentsHighlight);
    std::string buffer;
    std::string result;

    while (std::getline(stream, buffer))
    {
        if (!buffer.empty() && buffer[0] == '#')
        {
            buffer = "<span class=\"code-block-comment\">" + buffer + "</span>";
        }
        result += buffer + "\n";
    }

    return result;
}

std::string HttpServer::errorPage(const std::string &text, Notepad::Lang lang, const std::string& query)
{
    crow::mustache::context ctx;
    ctx["lang_code"] = Notepad::langToCode(lang);
    ctx["tagline"] = reinterpret_cast<const char*>(Notepad::WebUi::tagline(lang));
    ctx["text"] = text;
    ctx["go_back"] = reinterpret_cast<const char*>(Notepad::WebUi::configGoBack(lang));
    ctx["url"] = query;
    ctx["version"] = VERSION;

    static const auto templ = crow::mustache::compile(HTML_ERROR_PAGE);
    return templ.render_string(ctx);
}

std::string HttpServer::mainPage(Notepad::Lang lang)
{
    static std::map<Notepad::Lang, std::string> map;
    auto iter = map.find(lang);
    if (iter != map.end())
    {
        return iter->second;
    }

    crow::mustache::context ctx;
    ctx["lang_code"] = Notepad::langToCode(lang);
    ctx["tagline"] = reinterpret_cast<const char*>(Notepad::WebUi::tagline(lang));
    ctx["th_option"] = reinterpret_cast<const char*>(Notepad::WebUi::mainThOption(lang));
    ctx["th_value"] = reinterpret_cast<const char*>(Notepad::WebUi::mainThInput(lang));
    ctx["tunnel_name"] = reinterpret_cast<const char*>(Notepad::WebUi::mainTunnelName(lang));
    ctx["tunnel_name_p"] = reinterpret_cast<const char*>(Notepad::WebUi::mainTunnelName(lang));
    ctx["tunnel_type"] = reinterpret_cast<const char*>(Notepad::WebUi::mainTunnelType(lang));
    ctx["client_tcp"] = reinterpret_cast<const char*>(Notepad::WebUi::mainDropdownClientTCP(lang));
    ctx["client_udp"] = reinterpret_cast<const char*>(Notepad::WebUi::mainDropdownClientUDP(lang));
    ctx["server_tcp"] = reinterpret_cast<const char*>(Notepad::WebUi::mainDropdownServerTCP(lang));
    ctx["server_udp"] = reinterpret_cast<const char*>(Notepad::WebUi::mainDropdownServerUDP(lang));
    ctx["server_http"] = reinterpret_cast<const char*>(Notepad::WebUi::mainDropdownServerHTTP(lang));
    ctx["server_irc"] = reinterpret_cast<const char*>(Notepad::WebUi::mainDropdownServerIRC(lang));
    ctx["socks_proxy"] = reinterpret_cast<const char*>(Notepad::WebUi::mainDropdownSOCKSProxy(lang));
    ctx["http_proxy"] = reinterpret_cast<const char*>(Notepad::WebUi::mainDropdownHTTPProxy(lang));
    ctx["inbound"] = reinterpret_cast<const char*>(Notepad::WebUi::mainInbound(lang));
    ctx["outbound"] = reinterpret_cast<const char*>(Notepad::WebUi::mainOutbound(lang));
    ctx["length"] = reinterpret_cast<const char*>(Notepad::WebUi::mainLength(lang));
    ctx["quantity"] = reinterpret_cast<const char*>(Notepad::WebUi::mainQuantity(lang));
    ctx["variance"] = reinterpret_cast<const char*>(Notepad::WebUi::mainVariance(lang));
    ctx["b33"] = reinterpret_cast<const char*>(Notepad::WebUi::mainB33(lang));
    ctx["transient"] = reinterpret_cast<const char*>(Notepad::WebUi::mainTransient(lang));
    ctx["keepalive"] = reinterpret_cast<const char*>(Notepad::WebUi::mainKeepalive(lang));
    ctx["comments"] = reinterpret_cast<const char*>(Notepad::WebUi::mainComments(lang));
    ctx["generate"] = reinterpret_cast<const char*>(Notepad::WebUi::mainGenerate(lang));
    ctx["version"] = VERSION;

    const auto templ = crow::mustache::compile(HTML_MAIN_PAGE);
    map[lang] = templ.render_string(ctx);

    return map[lang];
}
